package com.util_code.utils.javajoda;

import com.util_code.utils.javajoda.format.JdtFormat;
import com.util_code.utils.javajoda.format.JdtFormatter;

import java.io.Serializable;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

public class JDateTime implements Comparable, Cloneable, Serializable {

    public static final String DEFAULT_FORMAT = "YYYY-MM-DD hh:mm:ss.mss";

    // day of week names
    public static final int MONDAY = 1;
    public static final int TUESDAY = 2;
    public static final int WEDNESDAY = 3;
    public static final int THURSDAY = 4;
    public static final int FRIDAY = 5;
    public static final int SATURDAY = 6;
    public static final int SUNDAY = 7;

    // month names
    public static final int JANUARY = 1;
    public static final int FEBRUARY = 2;
    public static final int MARCH = 3;
    public static final int APRIL = 4;
    public static final int MAY = 5;
    public static final int JUNE = 6;
    public static final int JULY = 7;
    public static final int AUGUST = 8;
    public static final int SEPTEMBER = 9;
    public static final int OCTOBER = 10;
    public static final int NOVEMBER = 11;
    public static final int DECEMBER = 12;

    /**
     * {@link DateTimeStamp} for current date.
     */
    protected DateTimeStamp time = new DateTimeStamp();

    /**
     * Day of week, range: [1-7] == [Monday - Sunday]
     */
    protected int dayofweek;

    /**
     * Day of year, range: [1-365] or [1-366]
     */
    protected int dayofyear;

    /**
     * Leap year flag.
     */
    protected boolean leap;

    /**
     * Week of year, range: [1-52] or [1-53]
     */
    protected int weekofyear;

    /**
     * Week of month.
     */
    protected int weekofmonth;

    /**
     * Current Julian Date.
     */
    protected JulianDateStamp jdate;

    /**
     * Julian Date for 1970-01-01T00:00:00 (Thursday).
     */
    public static final JulianDateStamp JD_1970 = new JulianDateStamp(2440587, 0.5);

    /**
     * Julian Date for 2001-01-01T00:00:00 (Monday).
     */
    public static final JulianDateStamp JD_2001 = new JulianDateStamp(2451910, 0.5);

    /**
     * Returns current {@link DateTimeStamp}. Returned instance is still used internally
     * (i.e. it is not cloned before returning).
     */
    public DateTimeStamp getDateTimeStamp() {
        return time;
    }

    /**
     * Loads current date time information.
     */
    public JDateTime setDateTimeStamp(final DateTimeStamp dts) {
        return set(dts.year, dts.month, dts.day, dts.hour, dts.minute, dts.second, dts.millisecond);
    }

    /**
     * Sets current Julian Date. This is the core of the JDateTime class and it
     * is used by all other classes. This method performs all calculations
     * required for whole class.
     * @param jds current julian date
     */
    public JDateTime setJulianDate(final JulianDateStamp jds) {
        setJdOnly(jds.clone());
        calculateAdditionalData();
        return this;
    }

    /**
     * Returns {@link JulianDateStamp}. It is the same instance used internally.
     */
    public JulianDateStamp getJulianDate() {
        return jdate;
    }

    /**
     * Returns JDN. Note that JDN is not equal to integer part of julian date. It is
     * calculated by rounding to the nearest integer.
     */
    public int getJulianDayNumber() {
        return jdate.getJulianDayNumber();
    }

    /**
     * Internal method for calculating various data other then date/time.
     */
    private void calculateAdditionalData() {
        this.leap = TimeUtil.isLeapYear(time.year);
        this.dayofweek = calcDayOfWeek();
        this.dayofyear = calcDayOfYear();
        this.weekofyear = calcWeekOfYear(firstDayOfWeek, mustHaveDayOfFirstWeek);
        this.weekofmonth = calcWeekNumber(time.day, this.dayofweek);
    }

    /**
     * Internal method that just sets the time stamp and not all other additional
     * parameters. Used for faster calculations only and only by main core
     * set/add methods.
     * @param jds julian date
     */
    private void setJdOnly(final JulianDateStamp jds) {
        jdate = jds;
        time = TimeUtil.fromJulianDate(jds);
    }

    /**
     * Core method that sets date and time. All others set() methods use this
     * one. Milliseconds are truncated after 3rd digit.
     * @param year year to set
     * @param month month to set
     * @param day day to set
     * @param hour hour to set
     * @param minute minute to set
     * @param second second to set
     */
    public JDateTime set(final int year, final int month, final int day, final int hour, final int minute, final int second, final int millisecond) {
        jdate = TimeUtil.toJulianDate(year, month, day, hour, minute, second, millisecond);

        if (TimeUtil.isValidDateTime(year, month, day, hour, minute, second, millisecond)) {
            time.year = year;
            time.month = month;
            time.day = day;
            time.hour = hour;
            time.minute = minute;
            time.second = second;
            time.millisecond = millisecond;
            calculateAdditionalData();
        } else {
            setJulianDate(jdate);
        }

        return this;
    }

    /**
     * Sets just Julian Date and no other parameter such as day of week etc.
     * Used internally for speed.
     * @param year year to set
     * @param month month to set
     * @param day day to set
     * @param hour hour to set
     * @param minute minute to set
     * @param second second to set
     */
    private void setJdOnly(final int year, final int month, final int day, final int hour, final int minute, final int second, final int millisecond) {
        setJdOnly(TimeUtil.toJulianDate(year, month, day, hour, minute, second, millisecond));
    }

    /**
     * Calculates day of week.
     */
    private int calcDayOfWeek() {
        int jd = (int) (jdate.doubleValue() + 0.5);
        return (jd % 7) + 1;
        //return (jd + 1) % 7;		// return 0 (Sunday), 1 (Monday),...
    }

    private static final int[] NUM_DAYS = {-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};        // 1-based
    private static final int[] LEAP_NUM_DAYS = {-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335};    // 1-based

    /**
     * Calculates day of year.
     */
    private int calcDayOfYear() {
        if (leap) {
            return LEAP_NUM_DAYS[time.month] + time.day;
        }
        return NUM_DAYS[time.month] + time.day;
    }


    /**
     * Calculates week of year. Based on:
     * "Algorithm for Converting Gregorian Dates to ISO 8601 Week Date"
     * by Rick McCarty, 1999
     * @param start first day of week
     * @param must must have day of week
     * @return week of year number
     */
    private int calcWeekOfYear(final int start, final int must) {

        // is modification required?
        // modification is a fix for the days of year because of the different
        // starting day of week. when modification is required, one week is added
        // or subtracted to the current day, so calculation of the week of year
        // would be correct.
        int delta = 0;
        if (start <= this.dayofweek) {
            if (must < start) {
                delta = 7;
            }
        } else {
            if (must >= start) {
                delta = -7;
            }
        }

        int jd = (int) (jdate.doubleValue() + 0.5) + delta;
        int WeekDay = (jd % 7) + 1;

        int time_year = time.year;
        int DayOfYearNumber = this.dayofyear + delta;
        if (DayOfYearNumber < 1) {
            time_year--;
            DayOfYearNumber = TimeUtil.isLeapYear(time_year) ? 366 + DayOfYearNumber : 365 + DayOfYearNumber;
        } else if (DayOfYearNumber > (this.leap ? 366 : 365)) {
            DayOfYearNumber = this.leap ? DayOfYearNumber - 366 : DayOfYearNumber - 365;
            time_year++;
        }

        // modification, if required, is finished. proceed to the calculation.

        int firstDay = jd - DayOfYearNumber + 1;
        int Jan1WeekDay = (firstDay % 7) + 1;

        // find if the date falls in YearNumber Y - 1 set WeekNumber to 52 or 53
        int YearNumber = time_year;
        int WeekNumber = 52;
        if ((DayOfYearNumber <= (8 - Jan1WeekDay)) && (Jan1WeekDay > must)) {
            YearNumber--;
            if ((Jan1WeekDay == must + 1) || ((Jan1WeekDay == must + 2) && (TimeUtil.isLeapYear(YearNumber)))) {
                WeekNumber = 53;
            }
        }

        // set WeekNumber to 1 to 53 if date falls in YearNumber
        int m = 365;
        if (YearNumber == time_year) {
            if (TimeUtil.isLeapYear(time_year)) {
                m = 366;
            }
            if ((m - DayOfYearNumber) < (must - WeekDay)) {
                YearNumber = time_year + 1;
                WeekNumber = 1;
            }
        }

        if (YearNumber == time_year) {
            int n = DayOfYearNumber + (7 - WeekDay) + (Jan1WeekDay - 1);
            WeekNumber = n / 7;
            if (Jan1WeekDay > must) {
                WeekNumber -= 1;
            }
        }
        return WeekNumber;
    }

    /**
     * Return the week number of a day, within a period. This may be the week number in
     * a year, or the week number in a month. Usually this will be a value >= 1, but if
     * some initial days of the period are excluded from week 1, because
     * minimalDaysInFirstWeek is > 1, then the week number will be zero for those
     * initial days. Requires the day of week for the given date in order to determine
     * the day of week of the first day of the period.
     * @param dayOfPeriod Day-of-year or day-of-month. Should be 1 for first day of
     * period.
     * @param dayOfWeek day of week
     * @return Week number, one-based, or zero if the day falls in part of the month
     * before the first week, when there are days before the first week because the
     * minimum days in the first week is more than one.
     */
    private int calcWeekNumber(final int dayOfPeriod, final int dayOfWeek) {
        // Determine the day of the week of the first day of the period
        // in question (either a year or a month).  Zero represents the
        // first day of the week on this calendar.
        int periodStartDayOfWeek = (dayOfWeek - firstDayOfWeek - dayOfPeriod + 1) % 7;
        if (periodStartDayOfWeek < 0) {
            periodStartDayOfWeek += 7;
        }

        // Compute the week number.  Initially, ignore the first week, which
        // may be fractional (or may not be).  We add periodStartDayOfWeek in
        // order to fill out the first week, if it is fractional.
        int weekNo = (dayOfPeriod + periodStartDayOfWeek - 1) / 7;

        // If the first week is long enough, then count it.  If
        // the minimal days in the first week is one, or if the period start
        // is zero, we always increment weekNo.
        if ((7 - periodStartDayOfWeek) >= minDaysInFirstWeek) {
            ++weekNo;
        }

        return weekNo;
    }

    /**
     * Adds time to current time. The main add method - all other add() methods
     * must use this one.
     * <p>
     * There are 2 different kinds of addings, when months are added:
     * <ul>
     * <li>when months are not specially concern. All remaining days will be
     * added to the next month. Example: 2003-01-31 + 0-01-0 = 2003-03-03</li>
     * <li>when months addition is fixed, and month is not approximated.
     * example: 2003-01-31 + 0-01-0 = 2003-02-28</li>
     * </ul>
     * @param year delta year
     * @param month delta month
     * @param day delta days
     * @param hour delta hours
     * @param minute delta minutes
     * @param second delta seconds
     * @param monthFix <code>true</code> for month fixing, <code>false</code> otherwise
     */
    public JDateTime add(final int year, final int month, final int day, final int hour, final int minute, final int second, final int millisecond, final boolean monthFix) {
        int difference = 0;
        if (trackDST) {
            difference = TimeZoneUtil.getOffset(this, timezone);
        }
        addNoDST(year, month, day, hour, minute, second, millisecond, monthFix);
        if (trackDST) {
            difference = TimeZoneUtil.getOffset(this, timezone) - difference;
            if (difference != 0) {
                addNoDST(0, 0, 0, 0, 0, 0, difference, false);
            }
        }

        return this;
    }

    protected void addNoDST(int year, int month, int day, int hour, int minute, int second, int millisecond, final boolean monthFix) {
        millisecond += time.millisecond;
        second += time.second;
        minute += time.minute;
        hour += time.hour;
        day += time.day;
        if (!monthFix) {
            month += time.month;
            year += time.year;
            set(year, month, day, hour, minute, second, millisecond);
        } else {
            // month fix:
            // 1. add all except month and year
            // 2. store day value
            // 3. add just months
            // 4. if new date is not equal to stored, return to last day of previous month
            setJdOnly(time.year, time.month, day, hour, minute, second, millisecond);
            int from = time.day;
            month += time.month + (year * 12);        // delta years to add are converted to delta months
            setJdOnly(time.year, month, time.day, time.hour, time.minute, time.second, time.millisecond);
            if (time.day < from) {
                set(time.year, time.month, 0, time.hour, time.minute, time.second, time.millisecond);
            } else {
                calculateAdditionalData();
            }

			/*
            // 5. store month value
			// 6. add just year
			// 7. if new month is not equal to stored, return to last day of previous month
			from = time.month;
			year += time.year;
			setJdOnly(year, time.month, time.day, time.hour, time.minute, time.second);
			if (time.month > from) {
				set(time.year, time.month, 0, time.hour, time.minute, time.second);
			}*/
        }
    }

    public JDateTime sub(final int year, final int month, final int day, final int hour, final int minute, final int second, final int millisecond, final boolean monthFix) {
        return add(-year, -month, -day, -hour, -minute, -second, -millisecond, monthFix);
    }


    /**
     * Performs time adding with preset value of monthFix attribute.
     * @param year delta year
     * @param month delta month
     * @param day delta days
     * @param hour delta hours
     * @param minute delta minutes
     * @param second delta seconds
     * @see #add(int, int, int, int, int, int, int, boolean)
     */
    public JDateTime add(final int year, final int month, final int day, final int hour, final int minute, final int second, final int millisecond) {
        return add(year, month, day, hour, minute, second, millisecond, monthFix);
    }

    public JDateTime sub(final int year, final int month, final int day, final int hour, final int minute, final int second, final int millisecond) {
        return add(-year, -month, -day, -hour, -minute, -second, millisecond, monthFix);
    }


    /**
     * Adds date, leaving time unchanged.
     * @param year years to add
     * @param month months to add
     * @param day days to add
     * @param monthFix <code>true</code> for month fixing, <code>false</code> otherwise
     * @see #add(int, int, int, int, int, int, int, boolean)
     */
    public JDateTime add(final int year, final int month, final int day, final boolean monthFix) {
        return add(year, month, day, 0, 0, 0, 0, monthFix);
    }

    public JDateTime sub(final int year, final int month, final int day, final boolean monthFix) {
        return add(-year, -month, -day, 0, 0, 0, 0, monthFix);
    }

    /**
     * Adds date, leaving time unchanged, with preset value of monthFix.
     * attribute.
     * @param year years to add
     * @param month months to add
     * @param day days to add
     * @see #add(int, int, int, boolean)
     */
    public JDateTime add(final int year, final int month, final int day) {
        return add(year, month, day, monthFix);
    }

    public JDateTime sub(final int year, final int month, final int day) {
        return add(-year, -month, -day, monthFix);
    }

    /**
     * Adds time.
     * @param hour hours to add
     * @param minute minutes to add
     * @param second seconds to add
     * @param monthFix <code>true</code> for month fixing, <code>false</code> otherwise
     * @see #add(int, int, int, int, int, int, int)
     */
    public JDateTime addTime(final int hour, final int minute, final int second, final int millisecond, final boolean monthFix) {
        return add(0, 0, 0, hour, minute, second, millisecond, monthFix);
    }

    public JDateTime subTime(final int hour, final int minute, final int second, final int millisecond, final boolean monthFix) {
        return add(0, 0, 0, -hour, -minute, -second, -millisecond, monthFix);
    }

    public JDateTime addTime(final int hour, final int minute, final int second, final boolean monthFix) {
        return add(0, 0, 0, hour, minute, second, 0, monthFix);
    }

    public JDateTime subTime(final int hour, final int minute, final int second, final boolean monthFix) {
        return add(0, 0, 0, -hour, -minute, -second, 0, monthFix);
    }


    /**
     * Adds time, with preset value of monthFix.
     * @param hour hours to add
     * @param minute minutes to add
     * @param second seconds to add
     * @see #addTime(int, int, int, int, boolean)
     */
    public JDateTime addTime(final int hour, final int minute, final int second, final int millisecond) {
        return addTime(hour, minute, second, millisecond, monthFix);
    }

    public JDateTime subTime(final int hour, final int minute, final int second, final int millisecond) {
        return addTime(-hour, -minute, -second, -millisecond, monthFix);
    }

    public JDateTime addTime(final int hour, final int minute, final int second) {
        return addTime(hour, minute, second, 0, monthFix);
    }

    public JDateTime subTime(final int hour, final int minute, final int second) {
        return addTime(-hour, -minute, -second, 0, monthFix);
    }


    /**
     * Adds year.
     * @param y year to add
     * @param monthFix <code>true</code> for month fixing, <code>false</code> otherwise
     */
    public JDateTime addYear(final int y, final boolean monthFix) {
        return add(y, 0, 0, monthFix);
    }

    public JDateTime subYear(final int y, final boolean monthFix) {
        return add(-y, 0, 0, monthFix);
    }

    /**
     * Adds year, with preset value of monthFix.
     * @param y year to add
     */
    public JDateTime addYear(final int y) {
        return addYear(y, monthFix);
    }

    public JDateTime subYear(final int y) {
        return addYear(-y, monthFix);
    }


    /**
     * Adds month.
     * @param m month to add
     * @param monthFix <code>true</code> for month fixing, <code>false</code> otherwise
     */
    public JDateTime addMonth(final int m, final boolean monthFix) {
        return add(0, m, 0, monthFix);
    }

    public JDateTime subMonth(final int m, final boolean monthFix) {
        return add(0, -m, 0, monthFix);
    }

    /**
     * Adds month, with preset value of monthFix.
     * @param m month to add
     */
    public JDateTime addMonth(final int m) {
        return addMonth(m, monthFix);
    }

    public JDateTime subMonth(final int m) {
        return addMonth(-m, monthFix);
    }

    /**
     * Adds days.
     * @param d days to add
     * @param monthFix <code>true</code> for month fixing, <code>false</code> otherwise
     */
    public JDateTime addDay(final int d, final boolean monthFix) {
        return add(0, 0, d, monthFix);
    }

    public JDateTime subDay(final int d, final boolean monthFix) {
        return add(0, 0, -d, monthFix);
    }

    /**
     * Adds days, with preset value of monthFix.
     * @param d days to add
     */
    public JDateTime addDay(final int d) {
        return addDay(d, monthFix);
    }

    public JDateTime subDay(final int d) {
        return addDay(-d, monthFix);
    }

    /**
     * Adds hours.
     * @param h hours to add
     * @param monthFix <code>true</code> for month fixing, <code>false</code> otherwise
     */
    public JDateTime addHour(final int h, final boolean monthFix) {
        return addTime(h, 0, 0, 0, monthFix);
    }

    public JDateTime subHour(final int h, final boolean monthFix) {
        return addTime(-h, 0, 0, 0, monthFix);
    }

    /**
     * Adds hours, with preset value of monthFix.
     * @param h hours to add
     */
    public JDateTime addHour(final int h) {
        return addHour(h, monthFix);
    }

    public JDateTime subHour(final int h) {
        return addHour(-h, monthFix);
    }


    /**
     * Adds minutes.
     * @param m minutes to add.
     * @param monthFix <code>true</code> for month fixing, <code>false</code> otherwise
     */
    public JDateTime addMinute(final int m, final boolean monthFix) {
        return addTime(0, m, 0, 0, monthFix);
    }

    public JDateTime subMinute(final int m, final boolean monthFix) {
        return addTime(0, -m, 0, 0, monthFix);
    }

    /**
     * Adds minutes, with preset value of monthFix.
     * @param m minutes to add.
     */
    public JDateTime addMinute(final int m) {
        return addMinute(m, monthFix);
    }

    public JDateTime subMinute(final int m) {
        return addMinute(-m, monthFix);
    }

    /**
     * Adds seconds.
     * @param s seconds to add
     * @param monthFix <code>true</code> for month fixing, <code>false</code> otherwise
     */
    public JDateTime addSecond(final int s, final boolean monthFix) {
        return addTime(0, 0, s, 0, monthFix);
    }

    public JDateTime subSecond(final int s, final boolean monthFix) {
        return addTime(0, 0, -s, 0, monthFix);
    }

    /**
     * Adds seconds, with preset value of monthFix.
     * @param s seconds to add
     */
    public JDateTime addSecond(final int s) {
        return addSecond(s, monthFix);
    }

    public JDateTime subSecond(final int s) {
        return addSecond(-s, monthFix);
    }


    /**
     * Adds milliseconds.
     * @param ms milliseconds to add
     * @param monthFix <code>true</code> for month fixing, <code>false</code> otherwise
     */
    public JDateTime addMillisecond(final int ms, final boolean monthFix) {
        return addTime(0, 0, 0, ms, monthFix);
    }

    public JDateTime subMillisecond(final int ms, final boolean monthFix) {
        return addTime(0, 0, 0, -ms, monthFix);
    }

    /**
     * Adds milliseconds, with preset value of monthFix.
     * @param ms milliseconds to add
     */
    public JDateTime addMillisecond(final int ms) {
        return addMillisecond(ms, monthFix);
    }

    public JDateTime subMillisecond(final int ms) {
        return addMillisecond(-ms, monthFix);
    }

    /**
     * Constructor that set date and time.
     * @param year year to set
     * @param month month to set
     * @param day day to set
     * @param hour hours to set
     * @param minute minutes to set
     * @param second seconds to set
     * @param millisecond milliseconds to set
     * @see #set(int, int, int, int, int, int, int)
     */
    public JDateTime(final int year, final int month, final int day, final int hour, final int minute, final int second, final int millisecond) {
        this.set(year, month, day, hour, minute, second, millisecond);
    }

    /**
     * Sets date, time is set to midnight (00:00:00.000).
     * @param year year to set
     * @param month month to set
     * @param day day to set
     */
    public JDateTime set(final int year, final int month, final int day) {
        return set(year, month, day, 0, 0, 0, 0);
    }

    /**
     * Constructor that sets just date. Time is set to 00:00:00.
     * @param year year to set
     * @param month month to set
     * @param day day to set
     * @see #set(int, int, int)
     */
    public JDateTime(final int year, final int month, final int day) {
        this.set(year, month, day);
    }

    /**
     * Sets time, date is unchanged.
     * @param hour hours to set
     * @param minute minutes to set
     * @param second seconds to set
     */
    public JDateTime setTime(final int hour, final int minute, final int second, final int millisecond) {
        return set(time.year, time.month, time.day, hour, minute, second, millisecond);
    }

    /**
     * Sets date, time remains unchanged.
     * @param year year
     * @param month month
     * @param day day
     */
    public JDateTime setDate(final int year, final int month, final int day) {
        return set(year, month, day, time.hour, time.minute, time.second, time.millisecond);
    }


    // ---------------------------------------------------------------- milliseconds

    /**
     * Constructor that sets current time specified as time in milliseconds, from
     * the midnight, January 1, 1970 UTC.
     * @param millis time in milliseconds, from the midnight, January 1, 1970 UTC
     * @see #setTimeInMillis(long)
     */
    public JDateTime(final long millis) {
        setTimeInMillis(millis);
    }

    /**
     * Sets the time based on current time in milliseconds. Current time is
     * calculated from the midnight, January 1, 1970 UTC.
     * <p>
     * Calculation is divided in two steps, due to precision issues.
     * @param millis time in milliseconds, from the midnight, January 1, 1970 UTC
     */
    public JDateTime setTimeInMillis(long millis) {
        millis += timezone.getOffset(millis);
        int integer = (int) (millis / TimeUtil.MILLIS_IN_DAY);
        double fraction = (double) (millis % TimeUtil.MILLIS_IN_DAY) / TimeUtil.MILLIS_IN_DAY;
        integer += JD_1970.integer;
        fraction += JD_1970.fraction;
        return setJulianDate(new JulianDateStamp(integer, fraction));
    }

    /**
     * Returns time based on current time in milliseconds. Current time is
     * calculated from the midnight, January 1, 1970 UTC.
     * <p>
     * Due to possible huge values calculation is divided in two steps:
     * first for fractional difference, and then for integer parts.
     * @return time in milliseconds, from the midnight, January 1, 1970 UTC
     */
    public long getTimeInMillis() {
        double then = (jdate.fraction - JD_1970.fraction) * TimeUtil.MILLIS_IN_DAY;
        then += (jdate.integer - JD_1970.integer) * TimeUtil.MILLIS_IN_DAY;
        then -= timezone.getOffset((long) then);
        then += then > 0 ? 1.0e-6 : -1.0e-6;
        return (long) then;
    }

    /**
     * Sets current year.
     * @param y year to set
     */
    public JDateTime setYear(final int y) {
        return setDate(y, time.month, time.day);
    }

    /**
     * Sets current month.
     * @param m month to set
     */
    public JDateTime setMonth(final int m) {
        return setDate(time.year, m, time.day);
    }

    /**
     * Sets current day of month.
     * @param d day to set
     */
    public JDateTime setDay(final int d) {
        return setDate(time.year, time.month, d);
    }

    /**
     * Set current hour.
     * @param h hour to set
     */
    public JDateTime setHour(final int h) {
        return setTime(h, time.minute, time.second, time.millisecond);
    }

    /**
     * Set current minute.
     * @param m minutes to set
     */
    public JDateTime setMinute(final int m) {
        return setTime(time.hour, m, time.second, time.millisecond);

    }

    /**
     * Sets current second.
     * @param s seconds and milliseconds to set
     */
    public JDateTime setSecond(final int s) {
        return setTime(time.hour, time.minute, s, time.millisecond);
    }

    public JDateTime setSecond(final int s, final int m) {
        return setTime(time.hour, time.minute, s, m);
    }

    /**
     * Sets current millisecond.
     * @param m milliseconds to set
     */
    public JDateTime setMillisecond(final int m) {
        return setTime(time.hour, time.minute, time.second, m);
    }

    /**
     * Returns current year.
     */
    public int getYear() {
        return time.year;
    }

    /**
     * Returns current month.
     */
    public int getMonth() {
        return time.month;
    }

    /**
     * Returns current day of month.
     * @see #getDayOfMonth
     */
    public int getDay() {
        return time.day;
    }

    /**
     * Returns current day of month.
     * @see #getDay
     */
    public int getDayOfMonth() {
        return time.day;
    }

    /**
     * Returns current hour.
     */
    public int getHour() {
        return time.hour;
    }

    /**
     * Returns current minutes.
     */
    public int getMinute() {
        return time.minute;
    }

    /**
     * Return current seconds. For an integer value, just cast the returned
     * value.
     */
    public int getSecond() {
        return time.second;
    }

    /**
     * Returns current milliseconds.
     */
    public int getMillisecond() {
        return time.millisecond;
    }

    /**
     * Returns current day of week.
     */
    public int getDayOfWeek() {
        return dayofweek;
    }

    /**
     * Returns current day of year.
     */
    public int getDayOfYear() {
        return dayofyear;
    }

    /**
     * Returns <code>true</code> if current year is leap year.
     */
    public boolean isLeapYear() {
        return leap;
    }

    /**
     * Returns current week of year.
     */
    public int getWeekOfYear() {
        return weekofyear;
    }

    /**
     * Returns current week of month.
     */
    public int getWeekOfMonth() {
        return weekofmonth;
    }


    /**
     * Returns the length of the specified month in days.
     */
    public int getMonthLength(final int month) {
        return TimeUtil.getMonthLength(time.year, month, this.leap);
    }

    /**
     * Returns the length of the current month in days.
     */
    public int getMonthLength() {
        return getMonthLength(time.month);
    }

    /**
     * Returns era: AD(1) or BC(0).
     */
    public int getEra() {
        return time.year > 0 ? 1 : 0;
    }

    /**
     * Calculates the number of milliseconds of a day.
     */
    public int getMillisOfDay() {
        return ((((time.hour * 60) + time.minute) * 60) + time.second) * 1000 + time.millisecond;
    }

    /**
     * Sets current local date and time.
     */
    public JDateTime setCurrentTime() {
        return setTimeInMillis(System.currentTimeMillis());
    }

    /**
     * Constructor that sets current local date and time.
     */
    public JDateTime() {
        this.setCurrentTime();
    }

    /**
     * Creates <code>JDateTime</code> from <code>Calendar</code>.
     */
    public JDateTime(final Calendar calendar) {
        setDateTime(calendar);
    }

    /**
     * Sets current date and time from <code>Calendar</code>.
     */
    public JDateTime setDateTime(final Calendar calendar) {
        setTimeInMillis(calendar.getTimeInMillis());
        changeTimeZone(calendar.getTimeZone());
        return this;
    }

    /**
     * Creates <code>JDateTime</code> from <code>Date</code>.
     */
    public JDateTime(final Date date) {
        setDateTime(date);
    }

    /**
     * Sets current date and time from <code>Date</code>.
     */
    public JDateTime setDateTime(final Date date) {
        return setTimeInMillis(date.getTime());
    }

    /**
     * Converts to <code>Date</code> instance.
     */
    public Date convertToDate() {
        return new Date(getTimeInMillis());
    }

    /**
     * Converts to <code>Calendar</code> instance.
     */
    public Calendar convertToCalendar() {
        Calendar calendar = Calendar.getInstance(getTimeZone());
        calendar.setTimeInMillis(getTimeInMillis());
        return calendar;
    }

    /**
     * Converts to <code>java.sql.Date</code> instance.
     */
    public java.sql.Date convertToSqlDate() {
        return new java.sql.Date(getTimeInMillis());
    }

    /**
     * Converts to <code>Time</code> instance.
     */
    public Time convertToSqlTime() {
        return new Time(getTimeInMillis());
    }

    /**
     * Converts to <code>Timestamp</code> instance.
     */
    public Timestamp convertToSqlTimestamp() {
        return new Timestamp(getTimeInMillis());
    }

    /**
     * Creates <code>JDateTime</code> from <code>DateTimeStamp</code>.
     */
    public JDateTime(final DateTimeStamp dts) {
        setDateTimeStamp(dts);
    }


    /**
     * Creates <code>JDateTime</code> from <code>JulianDateStamp</code>.
     */
    public JDateTime(final JulianDateStamp jds) {
        setJulianDate(jds);
    }

    /**
     * Creates <code>JDateTime</code> from <code>double</code> that represents JD.
     */
    public JDateTime(final double jd) {
        setJulianDate(new JulianDateStamp(jd));
    }

    /**
     * Creates <code>JDateTime</code> from a string.
     */
    public JDateTime(final String src) {
        parse(src);
    }

    /**
     * Creates <code>JDateTime</code> from a string, using specified template.
     */
    public JDateTime(final String src, final String template) {
        parse(src, template);
    }

    public JDateTime(final String src, final JdtFormat jdtFormat) {
        parse(src, jdtFormat);
    }


    protected boolean trackDST = JDateTimeDefault.trackDST;

    public boolean isTrackDST() {
        return trackDST;
    }

    public JDateTime setTrackDST(final boolean trackDST) {
        this.trackDST = trackDST;
        return this;
    }

    protected boolean monthFix = JDateTimeDefault.monthFix;

    /**
     * Returns <code>true</code> if month fix is active.
     */
    public boolean isMonthFix() {
        return monthFix;
    }

    /**
     * Sets custom month fix value.
     */
    public JDateTime setMonthFix(final boolean monthFix) {
        this.monthFix = monthFix;
        return this;
    }


    protected TimeZone timezone = JDateTimeDefault.timeZone == null ? TimeZone.getDefault() : JDateTimeDefault.timeZone;

    /**
     * Changes current timezone. Current time is changed if time zone has been changed.
     */
    public JDateTime changeTimeZone(final TimeZone timezone) {
        long now = getTimeInMillis();
        int difference = TimeZoneUtil.getOffsetDifference(now, this.timezone, timezone);
        this.timezone = timezone;
        if (difference != 0) {
            addMillisecond(difference);
        }
        return this;
    }

    /**
     * Changes time zone. Equivalent to:
     * <code>setTimeZone(from); changeTimeZone(to);</code>
     */
    public JDateTime changeTimeZone(final TimeZone from, final TimeZone to) {
        this.timezone = from;
        changeTimeZone(to);
        return this;
    }

    /**
     * Sets time zone <b>without</b> changing the time.
     */
    public JDateTime setTimeZone(final TimeZone timezone) {
        this.timezone = timezone;
        return this;
    }

    /**
     * Return currently active time zone.
     */
    public TimeZone getTimeZone() {
        return timezone;
    }

    /**
     * Returns <code>true</code> if current date is in
     * daylight savings time in the time zone.
     */
    public boolean isInDaylightTime() {
        long now = getTimeInMillis();
        int offset = timezone.getOffset(now);
        int rawOffset = timezone.getRawOffset();
        return (offset != rawOffset);
    }

    protected Locale locale = JDateTimeDefault.locale == null ? Locale.getDefault() : JDateTimeDefault.locale;

    /**
     * Sets custom locale.
     */
    public JDateTime setLocale(final Locale locale) {
        this.locale = locale;
        return this;
    }

    /**
     * Return currently active locale.
     */
    public Locale getLocale() {
        return locale;
    }

    protected String format = JDateTimeDefault.format;

    /**
     * Defines default format.
     */
    public JDateTime setFormat(final String format) {
        this.format = format;
        return this;
    }

    /**
     * Returns format.
     */
    public String getFormat() {
        return format;
    }


    protected JdtFormatter jdtFormatter = JDateTimeDefault.formatter;

    /**
     * Defines custom formatter.
     */
    public JDateTime setJdtFormatter(final JdtFormatter jdtFormatter) {
        this.jdtFormatter = jdtFormatter;
        return this;
    }

    /**
     * Returns actual {@link JdtFormatter}.
     */
    public JdtFormatter getJdtFormatter() {
        return jdtFormatter;
    }

    /**
     * Sets both format and formatter from provided {@link JdtFormat}.
     */
    public JDateTime setJdtFormat(final JdtFormat jdtFormat) {
        this.format = jdtFormat.getFormat();
        this.jdtFormatter = jdtFormat.getFormatter();
        return this;
    }

    /**
     * Returns string representation of date/time in specified format.
     */
    public String toString(final String format) {
        return jdtFormatter.convert(this, format);
    }

    /**
     * Returns spring representation of current date/time in currently active format.
     * @see #getFormat()
     */
    @Override
    public String toString() {
        return jdtFormatter.convert(this, format);
    }

    public String toString(final JdtFormat jdtFormat) {
        return jdtFormat.convert(this);
    }

    /**
     * Sets date/time from a string given in provided template.
     * @param src string containing date time information
     * @param format format template
     */
    public JDateTime parse(final String src, final String format) {
        setDateTimeStamp(jdtFormatter.parse(src, format));
        return this;
    }

    /**
     * Sets date/time from a string and currently active template.
     * @param src string containing date time information
     * @see #getFormat()
     */
    public JDateTime parse(final String src) {
        setDateTimeStamp(jdtFormatter.parse(src, format));
        return this;
    }

    public JDateTime parse(final String src, final JdtFormat jdtFormat) {
        setDateTimeStamp(jdtFormat.parse(src));
        return this;
    }

    /**
     * Checks if some string represents a valid date using actual template.
     * @return <code>true</code> if date is valid, otherwise <code>false</code>
     */
    public boolean isValid(final String s) {
        return isValid(s, format);
    }

    /**
     * Checks if some string represents a valid date using provided template.
     * @return <code>true</code> if date is valid, otherwise <code>false</code>
     */
    public boolean isValid(final String s, final String template) {
        DateTimeStamp dtsOriginal;
        try {
            dtsOriginal = jdtFormatter.parse(s, template);
        } catch (Exception ignore) {
            return false;
        }
        if (dtsOriginal == null) {
            return false;
        }
        return TimeUtil.isValidDateTime(dtsOriginal);
    }

    protected int firstDayOfWeek = JDateTimeDefault.firstDayOfWeek;
    protected int mustHaveDayOfFirstWeek = JDateTimeDefault.mustHaveDayOfFirstWeek;

    /**
     * Defines week definitions.
     * @param start first day in week, [1-7],
     * @param must must have day of the 1st week, [1-7]
     */
    public JDateTime setWeekDefinition(final int start, final int must) {
        if ((start >= 1) && (start <= 7)) {
            firstDayOfWeek = start;
        }
        if ((must >= 1) && (must <= 7)) {
            mustHaveDayOfFirstWeek = must;
            minDaysInFirstWeek = convertMin2Must(firstDayOfWeek, must);
        }
        return this;
    }

    /**
     * Returns actual the first day of the week.
     */
    public int getFirstDayOfWeek() {
        return firstDayOfWeek;
    }

    /**
     * Returns actual must have day of the 1st week.
     */
    public int getMustHaveDayOfFirstWeek() {
        return mustHaveDayOfFirstWeek;
    }

    protected int minDaysInFirstWeek = JDateTimeDefault.minDaysInFirstWeek;

    /**
     * Returns actual minimal number of days of the first week. It is
     * calculated from must have day of the first week.
     * @return minimal number of days of the first week
     */
    public int getMinDaysInFirstWeek() {
        return minDaysInFirstWeek;
    }

    /**
     * Defines week alternatively.
     * @param start first day in week
     * @param min minimal days of week
     */

    public JDateTime setWeekDefinitionAlt(final int start, final int min) {
        if ((start >= 1) && (start <= 7)) {
            firstDayOfWeek = start;
        }
        if ((min >= 1) && (min <= 7)) {
            mustHaveDayOfFirstWeek = convertMin2Must(firstDayOfWeek, min);
            minDaysInFirstWeek = min;
        }
        return this;
    }

    /**
     * Converts minimal day of week to must have day of week.
     * Method is symmetrical.
     * @param start first day of week
     * @param min minimal day of week
     * @return must have day of week
     */
    private static int convertMin2Must(final int start, final int min) {
        int must = 8 - min + (start - 1);
        if (must > 7) {
            must -= 7;
        }
        return must;
    }

    /**
     * Compares if two JDateTime instances are equal.
     * Comparison precision is 1e-3 seconds.
     */
    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (this.getClass() != obj.getClass()) {
            return false;
        }
        JDateTime jdt = (JDateTime) obj;
        return (this.monthFix == jdt.monthFix) && (this.firstDayOfWeek == jdt.firstDayOfWeek) && (this.mustHaveDayOfFirstWeek == jdt.mustHaveDayOfFirstWeek) && (this.time
                .equals(jdt.time)) && (this.timezone.equals(jdt.timezone));
    }

    @Override
    public int hashCode() {
        return HashCode.create()
                .hash(time)
                .hash(timezone)
                .hash(monthFix)
                .hash(firstDayOfWeek)
                .hash(mustHaveDayOfFirstWeek)
                .get();
    }

    @Override
    public JDateTime clone() {
        JDateTime jdt = new JDateTime(this.jdate);
        jdt.monthFix = this.monthFix;
        jdt.timezone = this.timezone;
        jdt.locale = this.locale;
        jdt.format = this.format;
        jdt.jdtFormatter = this.jdtFormatter;
        jdt.firstDayOfWeek = this.firstDayOfWeek;
        jdt.mustHaveDayOfFirstWeek = this.mustHaveDayOfFirstWeek;
        jdt.trackDST = this.trackDST;
        return jdt;
    }

    /**
     * Compares current JDateTime object with another one, up to 1 millisecond.
     * @param o JDateTime to compare
     * @return -1 if the current object is less than the argument, 0 if the argument is
     * equal, and 1 if the current object is greater than the argument
     */
    @Override
    public int compareTo(final Object o) {
        return time.compareTo(((JDateTime) o).getDateTimeStamp());
    }

    public int compareTo(final JDateTime jd) {
        return time.compareTo(jd.getDateTimeStamp());
    }

    /**
     * Compares current JDateTime date with another date. Time component is ignored.
     */
    public int compareDateTo(final JDateTime jd) {
        return time.compareDateTo(jd.getDateTimeStamp());
    }

    /**
     * Returns <code>true</code> if current time is after then provided time.
     */
    public boolean isAfter(final JDateTime then) {
        return time.compareTo((then).getDateTimeStamp()) > 0;
    }

    /**
     * Returns <code>true</code> if current time is before then provided time.
     */
    public boolean isBefore(final JDateTime then) {
        return time.compareTo((then).getDateTimeStamp()) < 0;
    }

    /**
     * Returns <code>true</code> if current date is after then provided date.
     * Time component is ignored.
     */
    public boolean isAfterDate(final JDateTime then) {
        return time.compareDateTo((then).getDateTimeStamp()) > 0;
    }

    /**
     * Returns <code>true</code> if current date is before then provided date.
     * Time component is ignored.
     */
    public boolean isBeforeDate(final JDateTime then) {
        return time.compareDateTo((then).getDateTimeStamp()) < 0;
    }

    /**
     * Returns number of full days between two dates.
     */
    public int daysBetween(final JDateTime then) {
        return this.jdate.daysBetween(then.jdate);
    }

    /**
     * Returns number of full days between two dates.
     */
    public int daysBetween(final JulianDateStamp then) {
        return this.jdate.daysBetween(then);
    }

    /**
     * Returns JD as double value.
     */
    public double getJulianDateDouble() {
        return jdate.doubleValue();
    }

    /**
     * Sets JD.
     */
    public JDateTime setJulianDate(final double jd) {
        return setJulianDate(new JulianDateStamp(jd));
    }

    /**
     * Returns <code>true</code> if provided date is equal to current one.
     * May be used for date validation test.
     */
    public boolean equalsDate(final int year, final int month, final int day) {
        return (time.year == year) && (time.month == month) && (time.day == day);
    }

    /**
     * Returns <code>true</code> if two dates are equal.
     * Time component is ignored.
     */
    public boolean equalsDate(final JDateTime date) {
        return time.isEqualDate(date.time);
    }

    /**
     * Returns <code>true</code> if two times are equal.
     * Date component is ignored.
     */
    public boolean equalsTime(final JDateTime date) {
        return time.isEqualTime(date.time);
    }

}
